﻿/*
* Copyright 2009-2010 AKABANA.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
* either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
using System;
using System.Globalization;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Akabana.Amf.Amf3.IO;
using Akabana.Amf.Exception;
using Akabana.Amf.IO;
using Akabana.Amf.Serializer;
using Akabana.Amf.Util;

namespace Akabana.Amf.Amf3.Serializer
{
    public class Amf3Deserializer : AbstractAmfDeserializer
    {
        public Amf3Deserializer(AmfReader amfReader)
            : base(amfReader)
        {
        }
        public override object Deserialize()
        {
            this.referenceClear();
            object result = this.DoDecode();

            return result;
        }

        private object DoDecode()
        {
            object result = null;

            var typeMarker = amfReader.ReadTypeMarker();
            switch (typeMarker)
            {
                case Amf3TypeMarker.UndefinedMarker:
                    result = null;
                    break;
                case Amf3TypeMarker.NullMarker:
                    result = null;
                    break;
                case Amf3TypeMarker.FalseMarker:
                    result = this.DoDecodeFalse();
                    break;
                case Amf3TypeMarker.TrueMarker:
                    result = this.DoDecodeTrue();
                    break;
                case Amf3TypeMarker.IntegerMarker:
                    result = this.DoDecodeIntger();
                    break;
                case Amf3TypeMarker.DoubleMarker:
                    result = this.DoDecodeNumber();
                    break;
                case Amf3TypeMarker.StringMarker:
                    result = this.DoDecodeStirng();
                    break;
                case Amf3TypeMarker.DateMarker:
                    result = this.DoDecodeDate();
                    break;
                case Amf3TypeMarker.ArrayMarker:
                    result = this.DoDecodeArray();
                    break;
                case Amf3TypeMarker.ObjectMarker:
                    result = this.DoDecodeObject();
                    break;
                case Amf3TypeMarker.ByteArrayMarker:
                    result = this.DoDecodeByteArray();
                    break;

                case Amf3TypeMarker.XmlDocMarker:
                case Amf3TypeMarker.XmlMarker:
                default:
                    var leftByteLength = amfReader.BaseStream.Length - amfReader.BaseStream.Position;
                    var leftBytes = amfReader.ReadBytes((int)leftByteLength);
                    var sb = new StringBuilder();
                    for (int i = 0; i < leftByteLength; i++)
                    {
                        sb.Append(leftBytes[i].ToString("X",CultureInfo.CurrentCulture));
                        sb.Append(' ');
                    }
                        
                    throw new UnsupportTypeMarkerException((byte)typeMarker,sb.ToString());
            }

            return result;
        }

        private bool DoDecodeTrue()
        {
            return true;
        }

        private bool DoDecodeFalse()
        {
            return false;
        }

        private int DoDecodeIntger()
        {
            return amfReader.ReadS29();
        }

        private double DoDecodeNumber()
        {
            return amfReader.ReadNumber();
        }

        private string DoDecodeStirng()
        {
            int u29 = amfReader.ReadU29();

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            string result = String.Empty;
            byte dataDefinition = (byte)(u29 & Amf3Consts.BIT_MASK_REFERENCE);
            switch (dataDefinition)
            {
                case Amf3Consts.BIT_VALUE:
                    {
                        var stringSize = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        if (stringSize > 0)
                        {
                            var stringData = new byte[stringSize];
                            amfReader.Read(stringData, 0, stringSize);

                            result = Encoding.UTF8.GetString(stringData, 0, stringSize);

                            referenceTable.SetString(result);
                        }
                        break;
                    }
                case Amf3Consts.BIT_REFERENCE:
                    {
                        var referenceIndex = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        result = referenceTable.GetString(referenceIndex);
                        break;
                    }
                default:
                    throw new IllegalDataDefinitionException(dataDefinition);
            }

            return result;
        }

        private DateTime DoDecodeDate()
        {
            int u29 = amfReader.ReadU29();

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            DateTime result;

            byte dataDefinition = (byte)(u29 & Amf3Consts.BIT_MASK_REFERENCE);
            switch (dataDefinition)
            {
                case Amf3Consts.BIT_VALUE:
                    {
                        long time = (long)amfReader.ReadNumber();
                        result = DateUtil.ToUtcDateTime(time);
                        referenceTable.SetObject(result);

                        break;
                    }
                case Amf3Consts.BIT_REFERENCE:
                    {
                        var index = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        result = (DateTime)referenceTable.GetObject(index);
                        break;
                    }
                default:
                    throw new IllegalDataDefinitionException(dataDefinition);
            }

            return result;
        }

        private Array DoDecodeArray()
        {
            var u29 = (int)amfReader.ReadU29();

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            object[] result = null;
            byte dataDefinition = (byte)(u29 & Amf3Consts.BIT_MASK_REFERENCE);
            switch (dataDefinition)
            {
                case Amf3Consts.BIT_VALUE:
                    {
                        string propertyName;
                        while (true)
                        {
                            propertyName = this.DoDecodeStirng();
                            if (propertyName.Length <= 0)
                            {
                                break;
                            }
                            else
                            {
                                throw new UnsupportTypeException(Amf3Consts.ECMA_ARRAY);
                            }
                        }

                        var size = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        result = new object[size];
                        referenceTable.SetObject(result);
                        if (size > 0)
                        {
                            for (int i = 0; i < size; i++)
                            {
                                result[i] = this.DoDecode();
                            }
                        }

                        break;
                    }
                case Amf3Consts.BIT_REFERENCE:
                    {
                        var referenceIndex = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        result = (object[])referenceTable.GetObject(referenceIndex);
                        break;
                    }
                default:
                    throw new IllegalDataDefinitionException(dataDefinition);
            }

            return result;
        }

        private object DoDecodeObject()
        {
            int u29 = amfReader.ReadU29();
            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();

            object result = new object();

            byte dataDefinition = (byte)(u29 & Amf3Consts.BIT_MASK_REFERENCE);
            switch (dataDefinition)
            {
                case Amf3Consts.BIT_VALUE:
                    {
                        Traits traits = DoDecodeTraits(u29);
                        do
                        {
                            if (traits == null)
                            {
                                throw new IllegalDataDefinitionException(dataDefinition);
                            }
                            if (traits.isExternalizable)
                            {
                                throw new UnsupportTypeException(Amf3Consts.EXTERNALIZABLE_OBJECT);
                            }
                            if (traits.isDynamic)
                            {
                                var map = new Dictionary<string, Object>();
                                while (true)
                                {
                                    string varName = DoDecodeStirng();
                                    if (varName.Length == 0)
                                    {
                                        break;
                                    }
                                    else
                                    {
                                        var value = DoDecode();
                                        map.Add(varName, value);
                                    }
                                }
                                result = map;
                                break;
                            }
                            {
                                var className = TypeResolver.FindRequestTypeName(traits.className);
                                var type = AssemblyLoader.FindType(className);
                                if (type.IsAMFValueType())
                                {
                                    if (type.IsEnum)
                                    {
                                        var values = DoDecodeObjectValues(traits);
                                        if (values.Length > 0)
                                        {
                                            result = values[0];
                                        }
                                    }
                                    else
                                    {
                                        var values = DoDecodeObjectValues(traits);
                                        if (values.Length > 0)
                                        {
                                            result = TypeUtil.CreateInstance(type, values);
                                        }
                                        else
                                        {
                                            result = TypeUtil.CreateInstance(type);
                                        }
                                    }
                                    referenceTable.SetObject(result);
                                }
                                else
                                {
                                    result = TypeUtil.CreateInstance(type);
                                    referenceTable.SetObject(result);
                                    var values = DoDecodeObjectValues(traits);
                                    PropertyUtil.BindProperties(result, traits.fields, values);
                                }
                            }
                        } while (false);

                        break;
                    }
                case Amf3Consts.BIT_REFERENCE:
                    {
                        var index = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        result = referenceTable.GetObject(index);
                        break;
                    }
                default:
                    throw new IllegalDataDefinitionException(dataDefinition);
            }

            return result;
        }

        private object[] DoDecodeObjectValues(Traits traits)
        {
            var numProperty = traits.fields.Length;
            var result = new object[numProperty];
            for (int i = 0; i < numProperty; i++)
            {
                result[i] = this.DoDecode();
            }
            return result;
        }

        private Traits DoDecodeTraits(int u29)
        {
            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            Traits result = null;
            byte traitDefinition = (byte)(u29 & Amf3Consts.BIT_MASK_TRAITS);
            switch (traitDefinition)
            {
                case Amf3Consts.BIT_TRAITS_VALUE:
                    {

                        result = new Traits();
                        

                        result.isExternalizable = (u29 & Amf3Consts.BIT_MASK_EXT) == Amf3Consts.BIT_EXT;
                        result.isDynamic = (u29 & Amf3Consts.BIT_MASK_DYNAMIC) == Amf3Consts.BIT_DYNAMIC;

                        if (result.isExternalizable)
                        {
                            throw new UnsupportTypeException("Externalizable");
                        }
                        else
                        {
                            result.className = this.DoDecodeStirng();
                            referenceTable.SetTraits(result);

                            var numProperties = u29 >> Amf3Consts.BIT_SHIFT_NUM_FIELDS;
                            var fields = new string[numProperties];
                            for (int i = 0; i < numProperties; i++)
                            {
                                fields[i] = this.DoDecodeStirng();
                            }
                            result.fields = fields;
                        }
                        break;
                    }
                case Amf3Consts.BIT_TRAITS_REFERENCE:
                    {
                        var index = u29 >> Amf3Consts.BIT_SHIFT_TRAITS;
                        result = referenceTable.GetTraits(index);
                        break;
                    }
                default:
                    throw new IllegalDataDefinitionException(traitDefinition);
            }

            return result;
        }

        private byte[] DoDecodeByteArray()
        {
            int u29 = amfReader.ReadU29();
            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            byte[] result = null;

            byte dataDefinition = (byte)(u29 & Amf3Consts.BIT_MASK_REFERENCE);
            switch (dataDefinition)
            {
                case Amf3Consts.BIT_VALUE:
                    {
                        var size = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        result = new byte[size];
                        referenceTable.SetObject(result);

                        amfReader.Read(result, 0, size);
                        break;
                    }
                case Amf3Consts.BIT_REFERENCE:
                    {
                        var referenceIndex = u29 >> Amf3Consts.BIT_SHIFT_REFERENCE;
                        result = (byte[])referenceTable.GetObject(referenceIndex);
                        break;
                    }
                default:
                    throw new IllegalDataDefinitionException(dataDefinition);
            }

            return result;
        }
        protected override IAmfReferenceTable CreateReferenceTable()
        {
            return new Amf3ReferenceTable();
        }
    }
}